package uk.ac.cam.cl.projectcharlie2010.clients.desktop.controller;

import java.awt.Frame;
import java.io.File;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.JOptionPane;
import javax.swing.JTextArea;

import uk.ac.cam.cl.projectcharlie2010.CharliesConstants;
import uk.ac.cam.cl.projectcharlie2010.camera.Camera;
import uk.ac.cam.cl.projectcharlie2010.clients.desktop.gui.Desktop;
import uk.ac.cam.cl.projectcharlie2010.game.GameController;
import uk.ac.cam.cl.projectcharlie2010.game.GameControllerResult;
import uk.ac.cam.cl.projectcharlie2010.game.player.Player;
import uk.ac.cam.cl.projectcharlie2010.game.player.PlayerEngine;
import uk.ac.cam.cl.projectcharlie2010.game.player.PlayerHuman;
import uk.ac.cam.cl.projectcharlie2010.game.player.physical.PhysicalPlayerListener;
import uk.ac.cam.cl.projectcharlie2010.game.player.physical.PlayerPhysical;
import uk.ac.cam.cl.projectcharlie2010.game.player.physical.VisualCalibrationException;
import uk.ac.cam.cl.projectcharlie2010.hardwaremove.PortLister;
import uk.ac.cam.cl.projectcharlie2010.physicalboard.FakePhysicalBoardController;
import uk.ac.cam.cl.projectcharlie2010.physicalboard.FakePopUpPhysicalBoardController;
import uk.ac.cam.cl.projectcharlie2010.physicalboard.PhysicalBoardController;
import uk.ac.cam.cl.projectcharlie2010.physicalboard.PhysicalBoardControllerListener;
import uk.ac.cam.cl.projectcharlie2010.physicalboard.PhysicalMoveCalculator;


import ch.nostromo.lib.controller.NosController;
import ch.nostromo.lib.controller.NosControllerException;
import ch.nostromo.lib.controller.NosResourcesException;
import ch.nostromo.lib.controller.NosSettingsException;
import ch.nostromo.tiffanys.engines.EngineSettings;
import ch.nostromo.tiffanys.engines.tiffanys.ai.OpeningBook;
import ch.nostromo.tiffanys.game.GameControllerException;
import ch.nostromo.tiffanys.game.GameControllerListener;
import ch.nostromo.tiffanys.game.board.Board;
import ch.nostromo.tiffanys.game.game.GameException;
import ch.nostromo.tiffanys.game.game.GameState;
import ch.nostromo.tiffanys.game.move.Move;
import ch.nostromo.tiffanys.game.move.MoveInput;
import ch.nostromo.tiffanys.game.move.MoveTools;
import ch.nostromo.tiffanys.game.pgn.PGN;
import ch.nostromo.tiffanys.game.player.PlayerException;
import ch.nostromo.tiffanys.game.player.PlayerTools;


/**
 * 
 * Top-level class that handles the control flow of much of the game.
 *
 */

public class DesktopController extends NosController implements GameControllerListener, PhysicalPlayerListener, PhysicalBoardControllerListener {

	private static Logger logger = Logger.getLogger(DesktopController.class.getName());

	private static DesktopController instance = null;

	//TODO: This isn't neat? Change?
	private final static File desktopSettingsFile = new File(System.getProperty("user.home") + "/.tiffanys", "desktop-" + uk.ac.cam.cl.projectcharlie2010.Charlies.VERSION + ".xml");

	private DesktopSettings settings = null;

	private GameController currentGameController;

	private Desktop desktop;
	
	//FIXME RAT new FakePopUpPhysicalBoardController(this);
	private PhysicalBoardController physController;


	/**
	 * Returns the one and only instance of the controller.
	 * 
	 * @return The instance value
	 */
	public static DesktopController getInstance() {
		if (instance == null) {
			instance = new DesktopController();
		}

		return instance;
	}

	/**
	 * Initialize the application (read parameters, start logging etc.)
	 * 
	 * @param args
	 *          Command line arguments
	 */
	public void init(String[] args) {
		try {
			
			//NEW - determine comm ports			
			Object[] possibleValues = PortLister.getPortStringArray();
			String port = (String)JOptionPane.showInputDialog(
			                    null,
			                    "Select the COM port used to connect to the arm from the following list.\n",
			                    "Select COM port for arm",
			                    JOptionPane.INFORMATION_MESSAGE, //TEST!
			                    null,
			                    possibleValues,
			                    possibleValues[possibleValues.length-1]);
			
			if (port == null) {
				throw new CharlieProgramLogicException("No COM port selected.");
			}
			
			physController = new PhysicalMoveCalculator(this, port);
			

			// Load client settings (if not found, try to save them for the first
			// time)
			try {
				settings = new DesktopSettings(desktopSettingsFile);
				File charlieSettingsDir = new File(CharliesConstants.SETTINGS_DIR_LOCATION);
				charlieSettingsDir.mkdir();
				
			} catch (NosSettingsException err) {
				settings = new DesktopSettings();
				settings.writeSettingsToXml(desktopSettingsFile);
				settings = new DesktopSettings(desktopSettingsFile);
			}

			this.initController(settings);

			// Start initializing
			logger.info("Starting Tiffanys " + uk.ac.cam.cl.projectcharlie2010.Charlies.VERSION + ".");

			//TODO: Reenable uncaught exception handler?
			//			logger.info("Appending Uncaught Exception handling ...");
			//			Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
			//
			//				@SuppressWarnings("synthetic-access")
			//				public void uncaughtException(Thread t, Throwable e) {
			//					String msg = "Uncaught Exception catched!";
			//					logger.log(Level.SEVERE, msg, e);
			//					showError(msg, e);
			//
			//				}
			//			});

			desktop = new Desktop();
			desktop.setVisible(true);

			this.translateComponents(desktop);

			
			//Opening new game menu instead of starting game immediately
			OpeningBook.getInstance();
			currentGameController = generateDefaultGameController();
			currentGameController.addGameControllerListener(this);
			desktop.newGameInvoked();
			

			
			// Create new Game
//			launchNewGame(generateDefaultGameController());


		} catch (NosSettingsException e) {
			System.err.println(e);
			logger.log(Level.SEVERE, "Unexpected Exception, program halted !", e);
			endApp();
		} catch (NosResourcesException e) {
			System.err.println(e);
			logger.log(Level.SEVERE, "Unexpected Exception, program halted !", e);
			endApp();
		} catch (NosControllerException e) {
			System.err.println(e);
			logger.log(Level.SEVERE, "Unexpected Exception, program halted !", e);
			endApp();
		}

	}

	public GameController generateDefaultGameController() throws NosSettingsException, NosResourcesException {
		// Start new game controller
		Player white = getDefaultWhitePlayer();
		Player black = getDefaultBlackPlayer();

		GameController newGame = new GameController(white, black);
		return newGame;
	}

	public Player getDefaultWhitePlayer() throws NosSettingsException, NosResourcesException {
		switch (settings.DEFAULT_PLAYERTYPE_FOR_WHITE) {
		case ENGINE:
			return getDefaultPlayerEngine();
		case HUMAN:
			return getDefaultPlayerHuman();
		case PHYSICAL:
			return getDefaultPlayerPhysical();
		default:
			throw new CharlieProgramLogicException("Unrecognised settings for default white player");
		}
	}

	public Player getDefaultBlackPlayer() throws NosResourcesException, NosSettingsException {
		switch (settings.DEFAULT_PLAYERTYPE_FOR_BLACK) {
		case ENGINE:
			return getDefaultPlayerEngine();
		case HUMAN:
			return getDefaultPlayerHuman();
		case PHYSICAL:
			return getDefaultPlayerPhysical();
		default:
			throw new CharlieProgramLogicException("Unrecognised settings for default white player");
		}
	}

	public PlayerHuman getDefaultPlayerHuman() throws NosResourcesException {
		return PlayerTools.createPlayerHuman(getTranslation("Common.HumanPlayerName"));
	}

	public PlayerEngine getDefaultPlayerEngine() throws NosSettingsException {
		return PlayerTools.createPlayerEngine(settings.DEFAULT_ENGINE_NAME, getDefaultEngineSettings());
	}

	//CHANGED: New method!
	public PlayerPhysical getDefaultPlayerPhysical() throws NosResourcesException {
		return PlayerTools.createPlayerPhysical(getTranslation("Common.PhysicalPlayerName"), this);
	}

	private EngineSettings getDefaultEngineSettings() throws NosSettingsException {
		String fileName = getSettings().DEFAULT_ENGINE_SETTINGS;
		return new EngineSettings(fileName);
	}

	public void launchNewGame(GameController gameController) {
		try {

			OpeningBook.getInstance();

			if (currentGameController != null) {
				currentGameController.removeGameControllerListener(this);
			}
		
			currentGameController = gameController;
			currentGameController.addGameControllerListener(this);

			desktop.newGameStarted();

			if (currentGameController.getCurrentPlayer() instanceof PlayerEngine) {
				this.computeNextMove();
			} else if (currentGameController.getCurrentPlayer() instanceof PlayerPhysical) {
				this.seeNextMove(); //CHANGED: new option for PlayerPhysical
			} else {
				desktop.openInterface(); //CHANGED: Refactored control flow
			}


		} catch (NosResourcesException e) {
			logger.log(Level.SEVERE, "Resources exception", e);
		}

	}

	public GameController getCurrentGameController() {
		return currentGameController;
	}

	/**
	 * computeNextMove is called to start up the engine to compute a move.
	 */
	
	public void computeNextMove() {
		//CHANGED: No call to closeInterface. This should be done elsewhere
		desktop.setGeneralStatusMessage(CharliesConstants.ENGINE_PLAYING);
		try {
			currentGameController.computeNextMove(true);
		} catch (GameControllerException e) {
			logger.log(Level.SEVERE, "Unexpected error:", e);
		}
	}

	public void engineMoveNow() {
		currentGameController.engineMoveNow();
	}

	private void announceGameEnd() {
		try {
			java.awt.Toolkit.getDefaultToolkit().beep();
			desktop.setGeneralStatusMessage(CharliesConstants.GAME_OVER);
			GameState gameState = currentGameController.getCurrentGameState();

			if (gameState.whiteMates()) {
				logger.info("Game finished. White mates!");
				desktop.announceGameEnd("1-0 {White mates}");
			} else if (gameState.blackMates()) {
				logger.info("Game finished. Black mates!");
				desktop.announceGameEnd("0-1 {Black mates}");
			} else if (gameState.remisByMaterial()) {
				logger.info("Game finished. Remis by material!");
				desktop.announceGameEnd("1/2-1/2 {Draw by material}");
			} else if (gameState.remisByFifty()) {
				logger.info("Game finished. Remis by 50 moves rule!");
				desktop.announceGameEnd("1/2-1/2 {Draw by repetition 50}");
			} else if (gameState.remisByThree()) {
				logger.info("Game finished. Remis by 3 times board repetition!");
				desktop.announceGameEnd("1/2-1/2 {Draw by position repetition 3}");
			} else if (gameState.remisByStaleMate()) {
				logger.info("Game finished. Remis by stale mate!");
				desktop.announceGameEnd("1/2-1/2 {Draw by Stale Mate}");
			}

		} catch (GameControllerException e) {
			logger.log(Level.SEVERE, "Unexpected error", e);
		}
	}

	public boolean isPromotionMove(MoveInput moveInput) {
		try {
			return currentGameController.isPromotionMove(moveInput);
		} catch (GameControllerException err) {
			logger.log(Level.SEVERE, "Unexpected error", err);
			return false;
		}
	}
	
	/**
	 * Used by GUI players to input moves to the game.
	 * Also handles transfer of control to next player after completion of the move.
	 * @param move
	 */

	public void enterMove(MoveInput move) {
		try {
			final Move moveAsMove = MoveTools.alterMove(move, getCurrentBoard(), getCurrentGameController().getCurrentColorToMove());
			final String FEN = getCurrentGameController().getFEN();
			
			currentGameController.enterNextMove(move);
			
			currentGameController.getCurrentPlayer().setActive(false);
			
			startPhysControllerToExecuteMove(moveAsMove, FEN);

			

			
			
			// Check for game end
			Player currentPlayer = currentGameController.getCurrentPlayer();
			if (currentGameController.getCurrentGameState().gameOpen()) {
				if (currentPlayer instanceof PlayerEngine) {
					desktop.closeInterface();
					desktop.enteredMoveAcceptedEngineTurn();
					desktop.engineIsComputing();
					this.computeNextMove(); //CHANGED: was currentGameController.computeNextMove(true); 
				} else if(currentPlayer instanceof PlayerPhysical) { //CHANGED: New section for physical player!
					desktop.closeInterface();
					desktop.enteredMoveAcceptedNonEngineTurn(currentPlayer);
					seeNextMove();
				} else {
					desktop.enteredMoveAcceptedNonEngineTurn(currentPlayer);
					//CHANGED: Interface should be open already //desktop.openInterface();
				}
			} else {
				desktop.enteredMoveAcceptedNonEngineTurn(currentPlayer);
				announceGameEnd();
			}
			
			//TODO: Neater solution?!
			//This is very counter intuitive: First the next relevant message is set, then this one.
			//Then automatic reversal to the previous message happens as arm finishes moving.
			desktop.setArmMovingStatusMessage();

		} catch (GameControllerException e) {
			desktop.enteredMoveRejected();
		}
	}
	
	/**
	 * Called to start the robot arm moving to act out the given move.
	 * 
	 * @param move, FEN
	 */

	private void startPhysControllerToExecuteMove(final Move move,
			final String FEN) {
		
		Thread physControllerRunner = new Thread() {
			@Override
			public void run() {
				physController.executePhysicalMove(move,FEN);
			}
		};
		
		System.out.println("EXECUTING PHYSICAL MOVE");
		
		physControllerRunner.start();
	}
	
	/**
	 * seeNextMove() is called to activate visual recognition of moves.
	 */

	//CHANGED: New method for activation of Physical player
	public void seeNextMove() {
		desktop.setGeneralStatusMessage(CharliesConstants.DO_PHYSICAL_MOVE);
		Player currentPlayer = currentGameController.getCurrentPlayer();
		if (currentPlayer instanceof PlayerPhysical) {
			try {
				((PlayerPhysical) currentPlayer).seeNextMove();
			} catch (VisualCalibrationException e) {
				java.awt.Toolkit.getDefaultToolkit().beep();
				calibrateCamera();
				try {
					((PlayerPhysical) currentPlayer).seeNextMove();
				} catch (VisualCalibrationException e1) {
					throw new CharlieProgramLogicException("Camera calibration is not working.");
				}
			}
		} else {
			throw new CharlieProgramLogicException("Non-physical player to do physical move");
		}
	}
	
	/**
	 * Displays information to the user when visual move recognition has failed.
	 */

	public void seeNextMoveFailed() {
		//TODO TEST?!
		java.awt.Toolkit.getDefaultToolkit().beep();
		JOptionPane.showMessageDialog(null, "Oops! I can't make sense of that.\nPlease restore the board to the state displayed on the screen, then press ok.");
		seeNextMove();
	}
	
	
	/**
	 * Called by the VisualMoveRecogniser when a move has been recognised.
	 * It inputs the move and handles transfer of control after move has been completed.
	 * Also checks move legality.
	 * 
	 * @param move
	 */

	//CHANGED: New method - end of seeMove
	public void seeNextMoveFinished(MoveInput move) {
		try {
			currentGameController.enterNextMove(move);

			if (currentGameController.getCurrentGameState().gameOpen()) {
				Player currentPlayer = currentGameController.getCurrentPlayer();
				if (currentPlayer instanceof PlayerEngine) {
					desktop.enteredMoveAcceptedEngineTurn();
					desktop.engineIsComputing();
					this.computeNextMove();
				} else if(currentPlayer instanceof PlayerPhysical) { //CHANGED: New section for physical player!
					//TODO: display status on desktop?
					desktop.enteredMoveAcceptedNonEngineTurn(currentPlayer);
					seeNextMove();
				} else {
					desktop.enteredMoveAcceptedNonEngineTurn(currentPlayer);
					desktop.openInterface();
				}
			} else {
				desktop.openInterface();
				announceGameEnd();
			}

		} catch (GameControllerException e) {
			//CHANGED: Removed desktop.enteredMoveRejected(); - this must only be related to graphical state of GUI?
			//(GUI will display old state)
			java.awt.Toolkit.getDefaultToolkit().beep();
			JOptionPane.showMessageDialog(null, "The move you made was illegal.\nPlease restore the board to the state it had before you made the move, then press ok.");
			System.out.println("You made an illegal move");
			seeNextMove(); //Try seeing a new different move!
		}
	}


	/**
	 * Disabled in GUI.
	 * 
	 * @throws GameException
	 */

	public void takeBackMove() throws GameException {
		currentGameController.takeBackMove();
		desktop.moveTakenBack();
		desktop.openInterface();
	}

	public void loadGame(File pgnFile) {
		try {
			GameController gc = PGN.importFirstPGN(pgnFile);
			currentGameController = gc;
			currentGameController.addGameControllerListener(this);
			desktop.gameLoaded();
			desktop.openInterface();

		} catch (GameControllerException e) {
			desktop.showMessage("Load failed!");
		}

	}

	public void saveGame(File pgnFile) {
		try {
			PGN.exportPGN(currentGameController, pgnFile);
			desktop.showMessage("Game saved.");
		} catch (GameControllerException e) {
			desktop.showMessage("Save failed!");
		}

	}

	public String getPGN() {
		try {
			return currentGameController.getPGN();
		} catch (GameControllerException e) {
			logger.log(Level.SEVERE, "Unexpected error", e);
			return "";
		}

	}

	public Move getLastMove() {
		return currentGameController.getLastMove();
	}

	public Board getCurrentBoard() {
		return currentGameController.getCurrentBoard();
	}

	public String getGameHeader() {
		try {
			return currentGameController.getPlayerWhite().getPlayerName() + "   -   " + currentGameController.getPlayerBlack().getPlayerName();
		} catch (PlayerException e) {
			logger.log(Level.SEVERE, "Unexpected error", e);
			return "";

		}

	}

	public void computeNextLegalMovesFinished(GameControllerResult gameControllerResult) {

		desktop.notifyComputeNextMoveDepthFinished(gameControllerResult);
	}
	
	/**
	 * 
	 * This method is called when the engine finishes its computation.
	 * It inputs the move the engine has decided on.
	 * 
	 */

	public void computeNextMoveFinished(GameControllerResult gameControllerResult) {
		try {
			desktop.notifyComputeNextMoveFinished(gameControllerResult);
			//ASSERTION: this result only comes from the currentGameController
			//- almost guaranteed to hold
			//Only breaks if user clicks "continue game" or "new game" in the very
			//short time from engine calling checkActive to calling this method

			currentGameController.getCurrentPlayer().setActive(false);
			
			startPhysControllerToExecuteMove(gameControllerResult.getSelectedMove(),gameControllerResult.getPreviousFEN());
			
			// Check for game end
			if (currentGameController.getCurrentGameState().gameOpen()) {
				if (currentGameController.getCurrentPlayer() instanceof PlayerEngine) {
					this.computeNextMove();
				} else if (currentGameController.getCurrentPlayer() instanceof PlayerPhysical) { //CHANGED: New option for PlayerPhysical
					this.seeNextMove();
				} else {
					desktop.openInterface();
				}
			} else {
				desktop.openInterface();
				announceGameEnd();
			}
			
			//TODO: Neater solution?!
			//This is very counter intuitive: First the next relevant message is set, then this one.
			//Then automatic reversal to the previous message happens as arm finishes moving.
			desktop.setArmMovingStatusMessage();

		} catch (GameControllerException err) {
			logger.log(Level.SEVERE, "Unexpected error", err);
		}

	}
	
	/**
	 * Is called by the PhysicalBoardController when a physical move has finished executing.
	 * Main function is to set players to active.
	 */

	public synchronized void executePhysicalMoveFinished() {
		//TODO REMOVE?!
		//this.notifyAll();
		
		System.out.println("FINISHED EXECUTING PHYSICAL MOVE");
		desktop.revertGeneralStatusMessage();
		
		currentGameController.getCurrentPlayer().setActive(true);
	}

	public void computeNextMoveDepthFinished(GameControllerResult engineResult) {
		desktop.notifyComputeNextMoveDepthFinished(engineResult);
	}

	public static void endApp() {
		logger.info("Shutdown Tiffanys.");
		System.exit(0);
	}

	public DesktopSettings getSettings() {
		return settings;
	}

	public void setSettings(DesktopSettings settings) {
		this.settings = settings;
	}

	public Desktop getDesktop() {
		return desktop;
	}

	/**
	 * Show error dialog box, if possible
	 * 
	 * @param msg
	 *          the msg
	 * @param err
	 *          the err
	 */
	public void showError(String msg, Throwable err) {
		try {

			Frame currentFrame = null;
			Frame[] frames = Frame.getFrames();
			for (int i = 0; i < frames.length; i++) {
				if (frames[i].isVisible()) {
					currentFrame = frames[i];
				}
			}

			StringWriter stringWriter = new StringWriter();
			PrintWriter w = new PrintWriter(stringWriter);
			err.printStackTrace(w);
			String message = "Please enter a bug report at http://sourceforge.net/tracker/?group_id=35984  \nwith a short description and the error stack trace.\n\n" + stringWriter.toString();
			JOptionPane.showMessageDialog(currentFrame, new JTextArea(message, 5, 10), "Uncaught Exception!", JOptionPane.ERROR_MESSAGE);

		} catch (Throwable ignored) {
			// ignore, as we're already in trouble :-(
		}

	}

	public void calibrateCamera() {	
		JOptionPane.showMessageDialog(null, "Please clear all pieces off the board, then press ok to update the background image.");
		PlayerPhysical.updateBackgroundForMoveRecognition();
		JOptionPane.showMessageDialog(null, "Please put the pieces in normal starting position, then press ok to finish calibrating the camera.");
		PlayerPhysical.calibrateVisualMoveRecogniser();
	}

}
